package com.ctrip.platform.dal.dao.client;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import com.ctrip.platform.dal.exceptions.DalRuntimeException;
import net.sf.cglib.core.ReflectUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionValidationException;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

import com.ctrip.platform.dal.dao.annotation.DalTransactional;
import com.ctrip.platform.dal.dao.annotation.Transactional;

public class DalTransactionalEnabler implements ImportBeanDefinitionRegistrar, BeanFactoryPostProcessor {
    private static final String BEAN_VALIDATOR_NAME = DalAnnotationValidator.VALIDATOR_NAME;
    private static final String BEAN_FACTORY_NAME = DalTransactionManager.class.getName();
    private static final String FACTORY_METHOD_NAME = "create";

    private BeanDefinitionRegistry registry;
    private ConfigurableListableBeanFactory beanFactory;

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        this.registry = registry;
        register();
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
        register();
    }

    private void register() {
        replaceBeanDefinition();
        registerValidator();
    }

    private String[] getBeanDefinitionNames() {
        return registry == null ? beanFactory.getBeanDefinitionNames() : registry.getBeanDefinitionNames();
    }

    private BeanDefinition getBeanDefinition(String beanName) {
        return registry == null ? beanFactory.getBeanDefinition(beanName) : registry.getBeanDefinition(beanName);
    }

    private void replaceBeanDefinition() {
        for (String beanName : getBeanDefinitionNames()) {
            BeanDefinition beanDef = getBeanDefinition(beanName);
            String beanClassName = beanDef.getBeanClassName();

            if (beanClassName == null || beanClassName.equals(BEAN_FACTORY_NAME))
                continue;

            Class beanClass;
            try {
                beanClass = Class.forName(beanDef.getBeanClassName());
            } catch (ClassNotFoundException e) {
                throw new BeanDefinitionValidationException("Cannot validate bean: " + beanName, e);
            }

            boolean annotated = false;

            List<Method> methods = new ArrayList<>();
            ReflectUtils.addAllMethods(beanClass, methods);
            List<Method> unsupportedMethods = new ArrayList<>();

            for (int i = 0; i < methods.size(); i++) {
                Method currentMethod = methods.get(i);

                if (isTransactionAnnotated(currentMethod)) {
                    if (Modifier.isFinal(currentMethod.getModifiers()) || Modifier.isStatic(currentMethod.getModifiers()) || Modifier.isPrivate(currentMethod.getModifiers()))
                        unsupportedMethods.add(currentMethod);
                    annotated = true;
                }
            }

            if (!unsupportedMethods.isEmpty()){
                StringBuilder errMsg=new StringBuilder();
                errMsg.append(String.format("The Methods below are not supported in dal transaction due to private, final or static modifier, please use public，protected or default modifier instead："));
                errMsg.append("\n");
                int index=1;
                for (Method method : unsupportedMethods) {
                    errMsg.append(String.format("%d. %s", index, method.toString()));
                    errMsg.append("\n");
                    index++;
                }
                throw new DalRuntimeException(errMsg.toString());
            }

            if (!annotated)
                continue;

            beanDef.setBeanClassName(BEAN_FACTORY_NAME);
            beanDef.setFactoryMethodName(FACTORY_METHOD_NAME);

            ConstructorArgumentValues cav = beanDef.getConstructorArgumentValues();

            if (cav.getArgumentCount() != 0)
                throw new BeanDefinitionValidationException("The transactional bean can only be instantiated with default constructor.");

            cav.addGenericArgumentValue(beanClass.getName());
        }
    }


    private boolean isTransactionAnnotated(Method method) {
        return method.isAnnotationPresent(Transactional.class) || method.isAnnotationPresent(DalTransactional.class);
    }

    private void registerValidator() {
        if (registry != null) {
            // Need to check here because bean name canbe low case
            if (registry.containsBeanDefinition(BEAN_VALIDATOR_NAME))
                return;

            for (String beanName : registry.getBeanDefinitionNames()) {
                BeanDefinition beanDef = registry.getBeanDefinition(beanName);
                if (Objects.equals(beanDef.getBeanClassName(), BEAN_VALIDATOR_NAME))
                    return;
            }

            registry.registerBeanDefinition(BEAN_VALIDATOR_NAME, BeanDefinitionBuilder.genericBeanDefinition(DalAnnotationValidator.class).getBeanDefinition());
        } else {
            // Need to check here because bean name canbe low case
            if (beanFactory.containsBeanDefinition(BEAN_VALIDATOR_NAME))
                return;

            for (String beanName : beanFactory.getBeanDefinitionNames()) {
                BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
                if (Objects.equals(beanDef.getBeanClassName(), BEAN_VALIDATOR_NAME))
                    return;
            }

            beanFactory.addBeanPostProcessor(new DalAnnotationValidator());
        }
    }
}